package org.example.utils;

import cn.hutool.core.map.MapUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.util.EntityUtils;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.TimeUnit;

import static cn.hutool.core.lang.Console.log;

/**
 * @author xuan
 * @create 2023/11/6
 * 注意这里的httpClient最终的执行结果response，一定要有一个response.close()关闭资源处理，否则可能会出现内存泄漏
 */
@Slf4j
public class HttpUtil {
    // 创建连接池管理器
    private static final PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager();
    // http客户端
    private static CloseableHttpClient httpClient;

    static {
        //连接池最大连接数
        connMgr.setMaxTotal(200);
        //每个路由的最大连接数
        connMgr.setDefaultMaxPerRoute(20);
        //连接的过期时间
        connMgr.setValidateAfterInactivity(1000*60);
        // 启动空闲连接清理线程
        startIdleConnectionMonitor();
    }

    /**
     * 启动空闲连接监控线程（守护线程）
     */
    private static void startIdleConnectionMonitor() {
        Thread monitor = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    // 每5秒检查一次
                    Thread.sleep(5000);
                    // 关闭过期连接
                    connMgr.closeExpiredConnections();
                    // 关闭空闲超10秒的连接
                    connMgr.closeIdleConnections(10, TimeUnit.SECONDS);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });
        // JVM退出时自动终止
        monitor.setDaemon(true);
        monitor.start();
    }

    /**
     * 不做证书校验的get请求方法：
     * 正常情况下比如我们java程序往github访问，我们的http请求里面需要有证书验证，以防止我们访问的github网站不是个钓鱼网站之类的
     * 但是这里我们把证书校验关掉，这样我们就能访问github了，但是安全性会降低
     */
    public static CloseableHttpResponse getNoCertificate(String url) {

        //注意，比如我们这里往github发送http请求的时候，因为github网站具有波动性，所以我们并不是每次访问都可以成功，那我们的定时任务每周六晚上11点进行访问数据入库
        //假如一次访问就是失败了，那么我们定时任务下次更新数据就要一周后了，因此我们需要配置一个http请求访问重发策略，每一次访问可能会有各种各样的异常Exception，我们
        //这里做了个重试策略，不管访问http请求遇到的是什么Exception异常，我们都会进行重试，重试十次之后，如果http请求仍然发送失败，则此http请求真返回失败，这样一次
        //定时任务，我们可以进行十次http请求的访问，直到成功读取数据。防止因网站时段波动性而带来的影响
        HttpRequestRetryHandler retryHandler = (exception, executionCount, context) -> {
            // 达到最大重试次数则停止重试
            if (executionCount >= 10) return false;

            // 仅对网络异常重试（如超时、连接中断）
//            if (exception instanceof ConnectTimeoutException ||
//                    exception instanceof SocketTimeoutException ||
//                    exception instanceof NoHttpResponseException) {
//                log.info("请求失败，第 {} 次重试...", executionCount);
//                return true;
//            }
//            return false; // 其他异常不重试

            //所有异常都重试，重试十次如果http请求还没有访问成功，则返回失败
            log.info("请求失败，第 {} 次重试...", executionCount);
            return true;
        };

        // 创建信任所有证书的策略
        TrustStrategy trustStrategy = (X509Certificate[] chain, String authType) -> true;
        SSLContext sslContext = null;
        try {
            sslContext = SSLContexts.custom().loadTrustMaterial(null, trustStrategy).build();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        } catch (KeyManagementException e) {
            throw new RuntimeException(e);
        } catch (KeyStoreException e) {
            throw new RuntimeException(e);
        }
        SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);

        // 创建HttpClient
        CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLSocketFactory(sslSocketFactory)
                .setRetryHandler(retryHandler) //http请求重试策略
                .build();

        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");
        httpGet.setHeader("Content-Type", "application/json; charset=utf-8");


        try {
            return  httpClient.execute(httpGet);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static CloseableHttpResponse get(String url) {
        if(httpClient == null) {
            synchronized(HttpUtil.class) {
                if(httpClient == null) {
                    httpClient = HttpClients.custom().setConnectionManager(connMgr).build();
                }
            }
        }

        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");
        httpGet.setHeader("Content-Type", "application/json; charset=utf-8");


        try {
            return  httpClient.execute(httpGet);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static CloseableHttpResponse get(String url, Map<String, String> headers) {
        if(httpClient == null) {
            synchronized(HttpUtil.class) {
                if(httpClient == null) {
                    httpClient = HttpClients.custom().setConnectionManager(connMgr).build();
                }
            }
        }

        HttpGet httpGet = new HttpGet(url);
        //设置请求头参数
        if (MapUtil.isNotEmpty(headers)) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                httpGet.setHeader(entry.getKey(), entry.getValue());
            }
        }

        try {
            return  httpClient.execute(httpGet);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static CloseableHttpResponse post(String url, Map<String, String> requestParams) {
        //这里之所以有两个检查是否为null的语句，主要是支持高并发的，比如现在一次性的有很多个请求过来，首先这些请求先判断第一个if，发现httpClient == null
        //是成立的，接着这些请求就会跑到synchronized关键字的上面，然后因为同步原因，第一个请求会先进去，进去之后发现httpClinet == null仍然是成立的，接着
        //当前请求就去给httpClient赋值；然后当前请求走出代码同步块，接着第二个请求会进去，发现httpClient已经不等于空了，所以就不会重新给httpClient进行赋值了，
        //因此第二个null是很有必要写的，可以提升代码的效率
        if(httpClient == null) {
            synchronized(HttpUtil.class) {
                if(httpClient == null) {
                    httpClient = HttpClients.custom().setConnectionManager(connMgr).build();
                }
            }
        }

        //创建Post请求
        HttpPost httpPost = new HttpPost(url);
        //设置请求头参数
        httpPost.setHeader("Accept", "application/json");
        httpPost.setHeader("Content-Type", "application/json; charset=utf-8");

        //给http请求设置请求参数。这里需要注意一个问题就是我们的map集合的参数一定要转换成json字符串，要不然服务端接收http请求的时候是没办法进行参数
        //转换的，到时候是会出现异常的，因此这里一定不要忘记使用JSON.toJSONString把map类型参数转换成json字符串类型的参数
        httpPost.setEntity(new StringEntity(JSON.toJSONString(requestParams), StandardCharsets.UTF_8));

        try {
            return  httpClient.execute(httpPost);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static String post(String url, Map<String, String> requestParams, Map<String, String> headers) {
        //这里之所以有两个检查是否为null的语句，主要是支持高并发的，比如现在一次性的有很多个请求过来，首先这些请求先判断第一个if，发现httpClient == null
        //是成立的，接着这些请求就会跑到synchronized关键字的上面，然后因为同步原因，第一个请求会先进去，进去之后发现httpClinet == null仍然是成立的，接着
        //当前请求就去给httpClient赋值；然后当前请求走出代码同步块，接着第二个请求会进去，发现httpClient已经不等于空了，所以就不会重新给httpClient进行赋值了，
        //因此第二个null是很有必要写的，可以提升代码的效率
        if(httpClient == null) {
            synchronized(HttpUtil.class) {
                if(httpClient == null) {
                    httpClient = HttpClients.custom().setConnectionManager(connMgr).build();
                }
            }
        }

        //创建Post请求
        HttpPost httpPost = new HttpPost(url);
        //设置请求头参数
        if (MapUtil.isNotEmpty(headers)) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                httpPost.setHeader(entry.getKey(), entry.getValue());
            }
        }

        //给http请求设置请求参数。这里需要注意一个问题就是我们的map集合的参数一定要转换成json字符串，要不然服务端接收http请求的时候是没办法进行参数
        //转换的，到时候是会出现异常的，因此这里一定不要忘记使用JSON.toJSONString把map类型参数转换成json字符串类型的参数
        httpPost.setEntity(new StringEntity(JSON.toJSONString(requestParams), StandardCharsets.UTF_8));

        //try with resources格式，会隐式的调用response.close，不用我们手动释放资源了
        try (CloseableHttpResponse response = httpClient.execute(httpPost);) {
            if (response != null && response.getStatusLine() != null) {
                return EntityUtils.toString(response.getEntity(), "UTF-8");
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return "";
    }

    public static String postXWwwFormUrlencoded(String url, Map<String, String> requestParams, Map<String, String> headers) {
        // 双重检查锁初始化httpClient（保持不变）
        if (httpClient == null) {
            synchronized (HttpUtil.class) {
                if (httpClient == null) {
                    httpClient = HttpClients.custom().setConnectionManager(connMgr).build();
                }
            }
        }

        HttpPost httpPost = new HttpPost(url);

        // 设置请求头（保持不变）
        if (MapUtil.isNotEmpty(headers)) {
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                httpPost.setHeader(entry.getKey(), entry.getValue());
            }
        }

        // ============== 关键修改：构建x-www-form-urlencoded请求体 ==============
        if (MapUtil.isNotEmpty(requestParams)) {
            // 将Map转为List<NameValuePair>
            List<NameValuePair> paramsList = new ArrayList<>();
            for (Map.Entry<String, String> entry : requestParams.entrySet()) {
                paramsList.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
            }
            // 创建表单实体（自动处理编码和Content-Type）
            httpPost.setEntity(new UrlEncodedFormEntity(paramsList, StandardCharsets.UTF_8));
        }
        // ============== 修改结束 ==============

        //try with resources格式，会隐式的调用response.close，不用我们手动释放资源了
        try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
            if (response != null && response.getStatusLine() != null) {
                return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            }
        } catch (IOException e) {
            throw new RuntimeException("请求失败: " + e.getMessage(), e);
        }
        return "";
    }

    public static void main1(String[] args) throws IOException {
        Map<String, String> requestParams = new HashMap<>();
        requestParams.put("name", "蜡笔小新没有心");
        requestParams.put("age", "10000");
        requestParams.put("address", "不在地球");

        CloseableHttpResponse response = HttpUtil.post("http://10.199.24.112:8080/insert", requestParams);

        System.out.println("返回响应码：" + response.getStatusLine().getStatusCode());
        HttpEntity entity = response.getEntity();
        if (entity != null) {
            String content = EntityUtils.toString(entity, StandardCharsets.UTF_8);
            System.out.println(content);
        }
        System.out.println(response);

        //CloseableHttpResponse对象http管理器连接池也就是PoolingHttpClientConnectionManager不会主动的释放，因此每次得到http请求响应结果并处理之后
        // ，必须要由我们手动的去释放CloseableHttpResponse对象
        //但是CloseableHttpClient这个http连接对象我们无需手动的释放，如果一个http连接对象在连接池中长时间没有被使用的时候，http连接池会把它自动释放掉
        //因此我们这里只需要写一个response.close()释放CloseableHttpResponse对象的语句即可，无需写释放http连接对象CloseableHttpClient的语句
        response.close();
    }

    public static void main(String[] args) {
        //获取临时邮箱后缀（也就是@后面的后缀），这些临时邮箱后缀的前缀可以随便写，不管写什么在特定的邮箱都可以登录，然后可以收取验证码
        //因此如果有用户通过拉新别的用户获得积分，就会通过临时邮箱一直伪造邮箱拉取积分；这里要做黑名单限制
        CloseableHttpResponse response = HttpUtil.getNoCertificate("https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/refs/heads/main/disposable_email_blocklist.conf");

        System.out.println("返回响应码：" + response.getStatusLine().getStatusCode());
        HttpEntity entity = response.getEntity();
        if (entity != null) {
            String content = null;
            try {
                content = EntityUtils.toString(entity, StandardCharsets.UTF_8);
                List<String> emailList = Arrays.asList(content.split("\n"));
                System.out.println(emailList);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(response);

        //CloseableHttpResponse对象http管理器连接池也就是PoolingHttpClientConnectionManager不会主动的释放，因此每次得到http请求响应结果并处理之后
        // ，必须要由我们手动的去释放CloseableHttpResponse对象
        //但是CloseableHttpClient这个http连接对象我们无需手动的释放，如果一个http连接对象在连接池中长时间没有被使用的时候，http连接池会把它自动释放掉
        //因此我们这里只需要写一个response.close()释放CloseableHttpResponse对象的语句即可，无需写释放http连接对象CloseableHttpClient的语句
        try {
            response.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
